package com.googlecode.perfectable.messageable;

import java.io.Closeable;
import java.io.OutputStream;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;

public class XMLTransmitter implements Transmitter, Closeable {
	private static final XMLOutputFactory XML_OUTPUT_FACTORY = createOutputFactory();
	private static final XMLEventFactory XML_EVENT_FACTORY = createEventFactory();
	
	private final XMLEventWriter writer;
	private final Marshaller marshaller;
	private final Set<Class<? extends Message>> knownClasses;
	private final MessageQualifier qualifier;

	public XMLTransmitter(OutputStream outputStream, Set<Class<? extends Message>> knownClasses) {
		this.knownClasses = knownClasses;
		this.writer = createWriter(outputStream);
		this.marshaller = createMarshaller(knownClasses);
		this.qualifier = MessageQualifier.create(knownClasses);
	}

	public void open() throws TransmissionException {
		try {
			Stream.initializeWrite(this.writer, XML_EVENT_FACTORY);
		}
		catch(XMLStreamException e) {
			throw new TransmissionException(e);
		}
	}
	
	@Override
	public <T extends Message> void send(T message) throws UnknownMessageException, TransmissionException {
		Class<T> messageClass = determineMessageClass(message);
		send(message, messageClass);
	}

	private <T extends Message> void send(T message, Class<T> messageClass) throws UnknownMessageException,
	TransmissionException {
		QName name = this.qualifier.nameForClass(messageClass);
		JAXBElement<? super T> je = new JAXBElement<>(name, messageClass, message);
		try {
			this.marshaller.marshal(je, this.writer);
			this.writer.add(XML_EVENT_FACTORY.createCharacters("\n"));
			this.writer.flush();
		}
		catch(JAXBException | XMLStreamException e) {
			throw new TransmissionException(e);
		}
	}
	
	private <T extends Message> Class<T> determineMessageClass(T message) throws UnknownMessageException {
		for(Class<? extends Message> knownClass : this.knownClasses) {
			if(knownClass.isInstance(message)) {
				@SuppressWarnings("unchecked")
				Class<T> casted = (Class<T>) knownClass;
				return casted;
			}
		}
		throw UnknownMessageException.ofClass(message.getClass());
	}

	@Override
	public void close() {
		try {
			this.writer.add(XML_EVENT_FACTORY.createEndDocument());
			this.writer.close();
		}
		catch(XMLStreamException e) {
			throw new RuntimeException(e); // TODO Auto-generated catch block
		}
	}
	
	private static XMLOutputFactory createOutputFactory() throws FactoryConfigurationError {
		XMLOutputFactory factory = XMLOutputFactory.newFactory();
		factory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, Boolean.TRUE);
		return factory;
	}
	
	private static XMLEventFactory createEventFactory() {
		return XMLEventFactory.newFactory();
	}

	private static XMLEventWriter createWriter(OutputStream outputStream) {
		try {
			return XML_OUTPUT_FACTORY.createXMLEventWriter(outputStream);
		}
		catch(XMLStreamException e) {
			throw new RuntimeException(e);
		}
	}
	
	private static Marshaller createMarshaller(Set<Class<? extends Message>> knownClasses) {
		Class<?>[] knownClassesArray = knownClasses.toArray(new Class<?>[knownClasses.size()]);
		JAXBContext jaxbContext;
		try {
			jaxbContext = JAXBContext.newInstance(knownClassesArray);
			Marshaller marshaller = jaxbContext.createMarshaller();
			marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
			return marshaller;
		}
		catch(JAXBException e) {
			throw new RuntimeException(e); // TODO Auto-generated catch block
		}
	}
	
}
